
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.Data;
using System.IO;
using System.Text;
using System.Xml;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;


using Swaf;
using Swaf.BizObj;
using Swaf.Container;
using Swaf.Logging;

namespace Swaf.DataAccess
{
	/// <summary>
	/// Summary description for AdoNetDataRequest.
	/// </summary>
	public class AdoNetDataRequest : IDataRequest, INameAddressSupport, ICatalogItem
	{
		protected Type m_dataResponseType = typeof(AdoNetDataResponse);
		protected IList m_sqlParams;
        protected IDictionary m_fieldMappings;
		protected string m_connectId;
		protected bool m_isStoredProc;
		protected string m_sqlInfo;
		protected bool m_hasResults;
		protected IAdoNetHelper m_helper;
		protected string m_defaultFormatterInfo;
		protected string m_catalogName;
		protected string m_catalogEntryName = null;

		protected int m_longRunningQueryThreshold=3000;
		protected int m_commandTimeout=60;

		public IApplication App { get { return Application.currentApp; } }

		public AdoNetDataRequest(XmlElement req, string catalogName)
		{
			m_catalogEntryName = catalogName;
			m_sqlParams = new ArrayList();
			m_connectId = req.GetAttribute("connection");
			m_defaultFormatterInfo = req.GetAttribute("defaultFormatter");
			m_helper = (IAdoNetHelper)Application.currentApp.factories["AdoNetHelpers"].
				createFromNickName(Application.currentApp.resMgr.resolve(req.GetAttribute("type")));
			m_longRunningQueryThreshold=(int) Application.currentApp.appData.get("LongRunningQueryThreshold-Log");

			try
			{
				string tmpTimeOut=req.GetAttribute("commandTimeout");
				m_commandTimeout=(tmpTimeOut==null || tmpTimeOut.Equals("")) ? m_commandTimeout:int.Parse(tmpTimeOut);
			}
			catch(Exception e)
			{
				throw new DataRequestException("Invalid property value for commandTimeout.  Only positive integers are acceptable for this property.",e);
			}
			foreach(XmlNode node in req.ChildNodes)
			{
				if(!(node is XmlElement))
					continue;
				XmlElement e = (XmlElement) node;
				string n = e.Name;
				if(n.CompareTo("Param") == 0)
				{
					string name = e.GetAttribute("name");
					string dbName = e.GetAttribute("dbName");
					if(dbName == "") 
						dbName = name;

					string direction = e.GetAttribute("direction");
					if(direction.Length == 0)
						direction = "Input";
					string paramType = e.GetAttribute("type");
					string strScale = e.GetAttribute("scale");
					bool optional=false;
					try
					{
						string op = e.GetAttribute("optional");
						if(op != null && op.Length != 0)
							optional = bool.Parse(op);
					}
					catch(Exception er){}

					int scale = 0;
					if(strScale != null && strScale.Length != 0)
						try 
						{		
							scale = Int32.Parse(strScale);
						}
						catch(Exception ex) 
						{
							throw new DataRequestException(String.Format("Invalid scale option specified for param '{0}': '{1}'", name, scale), ex);
						}
					
					m_sqlParams.Add(new Param(name, dbName, direction, paramType, scale,optional));
				}
                else if (n.CompareTo("FieldMapping") == 0)
                {
                    if (m_fieldMappings == null)
                        m_fieldMappings = new Hashtable();
                    if(e.GetAttribute("dbName") == "")
                        throw new DataRequestException("You must specify a dbName attribute for every field mapping and one does not for this dar");
                    if(e.GetAttribute("name") == "")
                        throw new DataRequestException("You must specify a name attribute for every field mapping and one does not for this dar");

                    m_fieldMappings[e.GetAttribute("dbName").ToUpper()] = e.GetAttribute("name").ToUpper().Replace(".","");
                }
                else if (n.CompareTo("SqlStoredProc") == 0)
                {
                    m_isStoredProc = true;
                    m_sqlInfo = e.GetAttribute("name");
                    m_hasResults = false;
                    if (String.Compare(e.GetAttribute("hasResults"), "true", true) == 0)
                        m_hasResults = true;
                }
                else if (n.CompareTo("SqlStatement") == 0)
                {
                    m_isStoredProc = false;
                    m_sqlInfo = e.InnerText.Trim();
                    m_hasResults = false;
                    if (String.Compare(e.GetAttribute("hasResults"), "true", true) == 0)
                        m_hasResults = true;
                }
			}
		}

		public void setDataResponseType(Type respType)
		{
			if (respType == null)
				m_dataResponseType = typeof(AdoNetDataResponse);
		
			m_dataResponseType = respType;

		}

		#region IDataRequest Members

		public virtual IDataResponse execute(INameAddressSupport info)
		{
			IDataResponse resp = null;
			IConnectionItem db = null;
			IDataReader reader = null;
			IApplication app = Application.currentApp;
			long start = Environment.TickCount;
			DateTime startTime = DateTime.Now;
			int retryCount=0;
			int retries = 0;
			bool done=false;
			try
			{
				//Try to resolve the DbConnectionCatalog macro to get the default catalog for connections.
				string DbConnectionCatalog = Application.currentApp.resMgr.resolve("$DbConnectionCatalog$");
				if (DbConnectionCatalog.Equals("$DbConnectionCatalog$"))
				{
					DbConnectionCatalog = "DB";
				}

				string dbKey = Application.currentApp.resMgr.resolve(m_connectId);
				//Support fully qualified connection names (DB.myConnection) so developers
				// can provide alternate connection catalogs.
				if (dbKey.IndexOf(".", 0) == -1)
				{
					dbKey = DbConnectionCatalog + "." + dbKey;
				}
				while(! done)
				{
					//Application.currentApp.Log.println("Db Key was " + dbKey);
					db = (IConnectionItem)app.appData.get(dbKey, 
						info.containsField("_transaction")?info.get("_transaction"):info);
					if(db == null)
					{
						// Throw exception because we didn't get back a valid connection.
						throw new DataRequestException(string.Format("Unable to retrieve IConnectionItem {0}.", dbKey), null);
					}

					//Get the retryCount from the connectionItem.
					retryCount=db.retryCount;

					IDbCommand cmd = ((IDbConnection)db.Connection).CreateCommand();
					cmd.CommandTimeout=m_commandTimeout;
					if(m_isStoredProc)
						cmd.CommandType = CommandType.StoredProcedure;
					cmd.Transaction = (IDbTransaction)db.ConnectionTrans;
					IDictionary resInfo = info as IDictionary;
					if(resInfo == null)
						resInfo = new Swaf.Container.NameAddressSupportDictionaryWrapper(info);
					cmd.CommandText =Application.currentApp.resMgr.resolve(m_sqlInfo, resInfo);

					object val;
					foreach(Param p in m_sqlParams)
					{
						if(p.direction == ParameterDirection.Output)
						{
							m_helper.addCommandParam(app, cmd, p.dbName, p.type, p.scale, p.direction);
						}
						else 
						{
							try
							{
								/*
								string extraInfo = null;
								object itemParent = null;
								if(NameAddress.isSingleAddress(name))
									itemParent = info;
								else
									itemParent = info.get(NameAddress.getAllButLast(name));
								if(itemParent is IBizObj)
								{
									IBizObj parent = (IBizObj)itemParent;
									IDecorator d =parent.getFieldDecoration(NameAddress.getLastAddress(name), "encrypt");
									if(d != null && String.Compare("full", d.Value, true) == 0)
										extraInfo = "encrypt";
								}

								val = checkParamType((extraInfo == null)?info.get(name):info.get(name, extraInfo), p);
								*/
								val = info.get(p.name, "externalTrusted");
							}
							catch(Exception e)
							{
								throw new DataRequestException(m_catalogEntryName, String.Format("Bad Param element with name '{0}' specified in DAR", p.name), e);
							}
							if((val == null || val == DBNull.Value) && !p.optional && !info.containsField(p.name))
							{
								throw new DataRequestException(m_catalogEntryName, String.Format("Param element named '{0}' specified in DAR, but was not passed as a value to execute.  Make sure you INameAddressSupport is populated with all the needed values such as this one", p.name), null);
							}
							//------------------------------------------------------------------------

							if(!(p.optional && (val == null || val == DBNull.Value)))
								m_helper.addCommandParam(app, cmd, p.dbName, p.type, p.scale, p.direction, val);
						}
					}
					Cmn.PerfMon.incrementCounter("ConcurrentDataRequests");
					if(m_hasResults)
					{
						try
						{
							reader = cmd.ExecuteReader();
						}
						catch(Exception dbE)
						{
							if (dbE.Message.StartsWith("General network error"))
							{
								//This is a valid retry condition, get a new connection and retry
								db.close();
								//Increment the retry count first.
								++retries;
								//Write the error to the trace log.
								Application.currentApp.Trace.write(TraceLevels.Normal, "Error executing request {0}. Error was {1}", m_catalogEntryName, dbE.ToString());
								//If we are out of retries, throw the error.
								if (retries > retryCount)
								{
									throw new DataRequestException(m_catalogEntryName, "General network error executing data command", dbE);
								}
								continue;
							}
							else
							{
								// This is an exception we aren't special 
								// Exception handling
								throw new DataRequestException(m_catalogEntryName, "Error executing data command", dbE);
							}
						}
						//Pass results to the DataResponse.
						resp = Cmn.createType(m_dataResponseType, new Type[]{typeof(AdoNetDataRequestInfo), typeof(string)},
							new object[]{new AdoNetDataRequestInfo(db,cmd, reader, info, m_sqlParams, m_fieldMappings, m_helper), m_defaultFormatterInfo}, true)
							as AdoNetDataResponse;
					}
					else
					{
                        try
                        {
                            try
                            {
                                cmd.ExecuteNonQuery();
                            }
                            catch (Exception dbE)
                            {
                                if (dbE.Message.StartsWith("General network error"))
                                {
                                    db.close();
                                    //This is a valid retry condition, get a new connection and retry
                                    //Increment the retry count first.
                                    ++retries;
                                    //Write the error to the trace log.
                                    Application.currentApp.Trace.write(TraceLevels.Normal, "Error executing request {0}. Error was {1}", m_catalogEntryName, dbE.ToString());
                                    //If we are out of retries, throw the error.
                                    if (retries > retryCount)
                                    {
                                        throw new DataRequestException(m_catalogEntryName, "General network error executing data command", dbE);
                                    }
                                    continue;
                                }
                                else
                                {
                                    // Not an exception we are prepared to handle so re-throw the exception
                                    // Exception handling
                                    throw new DataRequestException(m_catalogEntryName, "Error executing data command", dbE);
                                }
                            }
                            //Copy any out param values back into the info passed in.
                            foreach (Param p in m_sqlParams)
                                if (p.direction == ParameterDirection.Output || p.direction == ParameterDirection.InputOutput)
                                {
                                    try
                                    {
                                        info.put(p.name, m_helper.getParamValue(cmd.Parameters, p.dbName));
                                    }
                                    catch (Exception e)
                                    {
                                        app.Trace.write(BaseException.ExceptionTraceLevel, "Unable to Add Out Param {0}.  Exception was {1}.", p.name, e.ToString());

                                        //throw new DataRequestException(string.Format("Unable to Add Out Param {0}.  Exception was {1}.",p.name,e.Message), e);
                                    }
                                }
                        }
                        finally
                        {
                            resp = null;
                            db.release();
                            cmd.Dispose();
                        }
					}
					//If we make it this far, we have successfully executed the DAR and need to exit the while loop.
					done=true;
				}
			}
			catch(DataRequestException e)
			{
				Cmn.PerfMon.incrementCounter("DataAccessExceptions");
				throw;
			}
			catch(InvalidConnectionClassException e)
			{
				Cmn.PerfMon.incrementCounter("DataAccessExceptions");
				throw new DataRequestException(m_catalogEntryName, e);
			}
			catch(Exception e)
			{
				Cmn.PerfMon.incrementCounter("DataAccessExceptions");
				throw new DataRequestException(m_catalogEntryName, buildParamInfo(info), e);
			}
			finally
			{
				Cmn.PerfMon.decrementCounter("ConcurrentDataRequests");
			}
			long queryTime=Environment.TickCount-start;
			if (queryTime > m_longRunningQueryThreshold)
			{
				Application.currentApp.Log.println("Data Access Request {0} on thread {1} took {2} milliseconds to execute.", m_catalogEntryName, AppDomain.GetCurrentThreadId(), queryTime);
			}

			Cmn.PerfMon.incrementCounter("DataAccessRequestCompleted");
			Cmn.PerfMon.incrementCounter("AverageDataAccessCallLength",DateTime.Now.Ticks - startTime.Ticks);
			return resp;
		}

		protected virtual object checkParamType(object val, Param p)
		{
			if(val is Swaf.BGuid.Guid)
				if(p.type.EndsWith("Char"))
					return val.ToString();
				else
					return ((Swaf.BGuid.Guid)val).ToType(typeof(Guid), null);
			else if(val is IBizObj || val is IBizObjList)
			{
				Binary bin = null;
				try
				{
					BinaryFormatter formatter = new BinaryFormatter();
					bin = new Binary();
					formatter.Serialize(bin.DataStream, val);
					bin.DataStream.Seek(0, SeekOrigin.Begin);
					byte[] b = bin.Bytes;
					Console.WriteLine("Ado.NET binary param size is {0}", b.Length);
					val = b;
					//val = bin.Bytes;
				}
				finally
				{
					bin.Dispose();
				}

			}
			return val;
		}

		protected virtual string buildParamInfo(INameAddressSupport info)
		{
			StringBuilder buff = new StringBuilder(100);
			buff.Append("Params: ");
			int startLen = buff.Length;
			foreach(Param p in m_sqlParams)
			{
				if(p.direction==ParameterDirection.Output)
				{
					buff.AppendFormat("[out]{0}:{1}", p.name,p.type);
				}
				else 
				{
					object val = null;
					try
					{
						val = info.get(p.name);
					}
					catch(Exception e)
					{
						val = "<<ErrorValue>>";
					}
					string paramDirection = "[in]";
					if (p.direction==ParameterDirection.InputOutput)
					{
						paramDirection = "[inout]";
					}
					buff.AppendFormat("{0} {1}:{2} passed as '{3}'",paramDirection,p.name, p.type, val);
				}
				buff.AppendFormat(", ");
			}
			if(buff.Length > startLen)
				return buff.ToString(0, buff.Length-2);
			return buff.ToString();
		}

		IDataResponse Swaf.DataAccess.IDataRequest.execute(System.Collections.IDictionary info)
		{
			// TODO:  Add DbDataRequest.Swaf.DataAccess.IDataRequest.execute implementation
			return null;
		}

		#endregion

		#region INameAddressSupport Members

		public bool containsField(string nameAddress)
		{
			// TODO:  Add DbDataRequest.containsField implementation
			return false;
		}

		public bool isGetOnly
		{
			get
			{
				// TODO:  Add DbDataRequest.isGetOnly getter implementation
				return true;
			}
		}

		public Swaf.Container.XmlSerializationSupport SerializationSupport
		{
			get
			{
				// TODO:  Add DbDataRequest.SerializationSupport getter implementation
				return new Swaf.Container.XmlSerializationSupport ();
			}
		}

		public void put(string nameAddress, object val, object extraInfo)
		{
			get(nameAddress,val);
		}

		public void put(string nameAddress, object val)
		{
			get(nameAddress,val);
		}

		public string getXml(string options)
		{
			return null;
		}

		public object get(string nameAddress, object oneArg)
		{
			if (oneArg == null)
				return this;

			IDataResponse resp = null;
			if (oneArg is INameAddressSupport)
				resp = execute((INameAddressSupport)oneArg);
			else if (oneArg is IDictionary)
				resp = execute(new FlexiMap((IDictionary)oneArg, true));

			return (resp != null) ? resp.formatResults() : null;
		}

		public object get(string nameAddress)
		{
			return this.get(nameAddress, null);
		}

		public virtual object get(string nameAddress, params object[] infoArgs)
		{
			if (infoArgs != null && infoArgs.Length != 0)
				return get(nameAddress, infoArgs[0]);
			else
				return this;
		}

		public string Xml
		{
			get
			{
				// TODO:  Add DbDataRequest.Xml getter implementation
				return null;
			}
		}

		#endregion
		public IEnumerator GetEnumerator() { return null; }

		#region ICatalogItem Members
		protected string m_expiration=null;
		protected string m_key=null;
		protected string m_searchPath=null;

		public string catalogName
		{
			get
			{
				return m_catalogName;
			}
			set
			{
				m_catalogName=value;
			}
		}

		public string expiration
		{
			get
			{
				return m_expiration;
			}
			set
			{
				m_expiration=value;
			}
		}

		public string key
		{
			get
			{
				return m_key;
			}
			set
			{
				m_key=value;
			}
		}

		public string searchPath
		{
			get
			{
				return m_searchPath;
			}
			set
			{
				m_searchPath=value;
			}
		}

		public bool isCacheable
		{
			get { return true; }
		}

		#endregion
	}

    public class Param 
	{
		public Param(string name, string dbName, string dir, string type, int scale,bool optional) 
		{
			this.name = name;
			this.dbName = dbName;
			this.type = type;
			this.scale = scale;
			this.optional = optional;

			if(String.Compare(dir, "out", true) == 0)
				direction = ParameterDirection.Output;
			else if(String.Compare(dir, "in", true) == 0)
				direction = ParameterDirection.Input;
			else if(String.Compare(dir, "inout", true) == 0)
				direction = ParameterDirection.InputOutput;
			else
				direction = (ParameterDirection)Enum.Parse(typeof(ParameterDirection), dir, true);
		}

		public string name;
		public string dbName;
		public string type;
		public int scale;
		public bool optional;
		public ParameterDirection direction;
	}

	public class AdoNetDataRequestInfo
	{
        protected static IDictionary s_emptyFieldMapping = new Hashtable();
		protected IConnectionItem m_db;
		protected IDataReader m_reader;
		protected INameAddressSupport m_passedInfo;
		protected IDbCommand m_cmd;
		protected IList m_sqlParams;
		protected IAdoNetHelper m_helper;
        protected IDictionary m_fieldMappings;

        public AdoNetDataRequestInfo(IConnectionItem db, IDbCommand cmd, IDataReader reader, INameAddressSupport passedInfo, IList sqlParams, IDictionary fieldMappings, IAdoNetHelper helper)
		{
			m_db = db;
			m_cmd = cmd;
			m_reader = reader;
			m_passedInfo = passedInfo;
			m_sqlParams = sqlParams;
            m_fieldMappings = fieldMappings;
			m_helper = helper;
		}

		public IConnectionItem DbConnection {get{return m_db;}}
		public IDataReader DataReader {get{return m_reader;}}
		public IDbCommand DbCommand {get{return m_cmd;}}
		public IList SqlParams {get{return m_sqlParams;}}
        public IDictionary FieldMappings { get { return m_fieldMappings==null?s_emptyFieldMapping:m_fieldMappings; } }
		public IAdoNetHelper ADONetHelper{get{return m_helper;}}

		public object getPassedItem(string key)
		{
			return m_passedInfo.get(key);
		}
		public void putPassedItem(string key,object val)
		{
			m_passedInfo.put(key,val);
			return;
		}

		public INameAddressSupport PassedInfo { get { return m_passedInfo; } }
	}

}
